var express = require('express');
var bodyParser = require('body-parser');
var formidable = require('formidable'),
    http = require('http'),
    fs = require('fs'),
    vhost = require('vhost');
var Q = require('q');
var Vacation = require('./models/vacation.js'),
    VacationInSeasonListener = require('./models/vacationInSeasonListener.js');

var Dealer = require('./models/dealer.js'),
    geocode = require('./lib/geocode.js');

var app = express();

var credentials = require('./credentials.js');

var emailService = require('./lib/email.js')(credentials);

var handlebars = require('express-handlebars').create(
    {
        defaultLayout: 'main',
        extname: '.hbs',
        helpers: {
            section: function (name, options) {
                if (!this._sections) this._sections = {};
                this._sections[name] = options.fn(this);
                return null;
            },
            static: function (name) {
                return require('./lib/static.js').map(name);
            }
        }
    });
app.engine('.hbs', handlebars.engine);
app.set('view engine', '.hbs');

// set up css/js bundling
var bundler = require('connect-bundle')(require('./config.js'));
app.use(bundler);

app.set('port', process.env.PORT || 3000);

app.enable('trust proxy');

app.use(express.static(__dirname + '/public'));
app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json());

var MongoSessionStore = require('session-mongoose')(require('connect'));
var sessionStore = new MongoSessionStore({
    url: credentials.mongo.development.connectionString
});

app.use(require('cookie-parser')(credentials.cookieSecret));
app.use(require('express-session')({
    secret: credentials.sessionSecret,
    store: sessionStore,
    resave: true,
    saveUninitialized: true
}));

app.use(require('csurf')());
app.use(function (req, res, next) {
    res.locals._csrfToken = req.csrfToken();
    next();
});

var mongoose = require('mongoose');
var opts = {
    server: {
        socketOptions: {keepAlive: 1}
    }
};

switch (app.get('env')) {
    case 'development':
        app.use(require('morgan')('dev'));
        mongoose.connect(credentials.mongo.development.connectionString, opts);
        break;
    case 'production':
        app.use(require('express-logger')({path: __dirname + '/log/requests.log'}));
        mongoose.connect(credentials.mongo.production.connectionString, opts);
        break;
    default :
        throw new Error('Unknown execution environment: ' + app.get('env'));
}

app.use(function (req, res, next) {
    var domain = require('domain').create();
    domain.on('error', function (err) {
        console.error('DOMAIN ERROR CAUGHT\n', err.stack);
        try {
            //5秒内进行故障保护性关机
            setTimeout(function () {
                console.error('Failsafe shutdown.');
                process.exit(1);
            }, 5000);

            //从集群中断开
            var worker = require('cluster').worker;
            if (worker) worker.disconnect();
            server.close();

            try {
                //尝试使用Express错误路由
                next(err);
            } catch (error) {
                console.error('Express error mechanism failed.\n', error.stack);
                res.statusCode(500);
                res.setHeader('content-type', 'text-plain');
                res.end('Server error.');
            }
        } catch (error) {
            console.error('Unable to send 500 response.\n', error.stack);
        }
    });

    domain.add(req);
    domain.add(res);

    domain.run(next);
});

var static = require('./lib/static.js').map;
app.use(function (req, res, next) {
    var now = new Date();
    res.locals.logoImage = now.getMonth() == 11 && now.getDate() == 19 ? static('/img/logo_bud_clark.png') : static('/img/logo.png');
    next();
});

Vacation.find(function (err, vacations) {
    if (vacations.length) return;

    new Vacation({
        name: 'Hood River Day Trip',
        slug: 'Hood-river-day-trip',
        category: 'Day Trip',
        sku: 'HR199',
        description: 'Spend a day sailing on the Columbia and enjoying craft beers in Hood River!',
        priceInCents: 9995,
        tags: ['day trip', 'hood river', 'sailing', 'windsurfing', 'breweries'],
        inSeason: true,
        maximumGuests: 16,
        available: true,
        packagesSold: 0
    }).save();

    new Vacation({
        name: 'Oregon Coast Getaway',
        slug: 'oregon-coast-getaway',
        category: 'Weekend Getaway',
        sku: 'OC39',
        description: 'Enjoy the ocean air and quaint coastal towns!',
        priceInCents: 269995,
        tags: ['weekend getaway', 'oregon coast', 'beachcombing'],
        inSeason: false,
        maximumGuests: 8,
        available: true,
        packagesSold: 0
    }).save();

    new Vacation({
        name: 'Rock Climbing in Bend',
        slug: 'rock-climbing-in-bend',
        category: 'Adventure',
        sku: 'B99',
        description: 'Experience the thrill of rock climbing in the high desert.',
        priceInCents: 289995,
        tags: ['weekend getaway', 'bend', 'high desert', 'rock climbing', 'hiking', 'skiing'],
        inSeason: true,
        requiresWaiver: true,
        maximumGuests: 4,
        available: false,
        packagesSold: 0,
        notes: 'The tour guide is currently recovering from a skiing accident.'
    }).save();
});

Dealer.find(function (err, dealers) {
    if (dealers.length) return;

    new Dealer({
        name: 'ken.c',
        address1: '竹园路298号',
        address2: '',
        city: '苏州',
        state: '高新区',
        zip: '215300',
        country: '中国',
        phone: '',
        website: '',
        active: true,
        geocodeAddress: '',
        lat: 0,
        lng: 0
    }).save();
    new Dealer({
        name: 'ken',
        address1: '墩头镇',
        address2: '',
        city: '南通',
        state: '海安',
        zip: '',
        country: '中国',
        phone: '',
        website: '',
        active: true,
        geocodeAddress: '',
        lat: 0,
        lng: 0
    }).save();
});

var dealerCache = {
    lastRefreshd: 0,
    refreshInterval: 60 * 60 * 1000,
    jsonUrl: '/dealers.json',
    geocodeLimit: 2000,
    geocodeCount: 0,
    geocodeBegin: 0
};
dealerCache.jsonFile = __dirname + '/public' + dealerCache.jsonUrl;

function geocodeDealer(dealer) {
    var addr = dealer.getAddress(' ');
    if (addr === dealer.geocodeAddress) return;  //已经处理过
    if (dealerCache.geocodeCount >= dealerCache.geocodeLimit) {

        //距离前一次地理编码过去24小时?
        if (Date.now() > dealerCache.geocodeBegin + 24 * 60 * 60 * 1000) {
            dealerCache.geocodeBegin = Date.now();
            dealerCache.geocodeCount = 0;
        } else {
            return;
        }
    }
    geocode(addr, credentials, function (err, coords) {
        if (err) return console.log('Geocoding failure for ' + addr);
        dealer.lat = coords.lat;
        dealer.lng = coords.lng;
        dealer.geocodeAddress = addr;
        dealer.save();
        dealerCache.geocodeCount = dealerCache.geocodeCount + 1;
        dealerCache.lastRefreshd = Date.now();
    });
}

dealerCache.refresh = function (cb) {
    if (Date.now() > dealerCache.lastRefreshd + dealerCache.refreshInterval) {
        Dealer.find({active: true}, function (err, dealers) {
            if (err) return console.log('Error fetching dealers: ' + err);
            dealers.forEach(geocodeDealer);

            fs.writeFileSync(dealerCache.jsonFile, JSON.stringify(dealers));

            cb();

        })
    }
};

function refreshDealerCacheForever() {
    dealerCache.refresh(function () {
        setTimeout(refreshDealerCacheForever, dealerCache.refreshInterval);
    });
}

if (!fs.existsSync(dealerCache.jsonFile)) {
    fs.writeFileSync(JSON.stringify([]));
}
refreshDealerCacheForever();

app.use(function (req, res, next) {
    res.locals.showTests = app.get('env') !== 'production' && req.query.test === '1';
    next();
});

app.use(function (req, res, next) {
    var cluster = require('cluster');
    if (cluster.isWorker) {
        console.log('Worker %d received request', cluster.worker.id);
    }
    next();
});

app.use('/api', require('cors')());

var getWeatherData = (function () {
    var c = {
        refreshed: 0,
        refreshing: false,
        updateFrequency: 360000,  //an hour
        locations: [
            {name: 'Portland'},
            {name: 'Bend'},
            {name: 'Manzanita'}
        ]
    };
    return function () {
        if (!c.refreshing && Date.now() > c.refreshed + c.updateFrequency) {
            c.refreshing = true;
            var promises = [];
            c.locations.forEach(function (loc) {
                var deferred = Q.defer();
                var url = 'http://api.wunderground.com/api/' + credentials.weatherUnderground.apiKey + '/conditions/q/OR/' + loc.name + '.json';
                http.get(url, function (res) {
                    var body = '';
                    res.on('data', function (chunk) {
                        body += chunk;
                    });
                    res.on('end', function () {
                        body = JSON.parse(body);
                        loc.forecastUrl = body.current_observation.forecast_url;
                        loc.iconUrl = body.current_observation.icon_url;
                        loc.weather = body.current_observation.weather;
                        loc.temp = body.current_observation.temperature_string;
                        deferred.resolve();
                    });
                });
                promises.push(deferred);
            });
            Q.all(promises).then(function () {
                c.refreshing = false;
                c.refreshed = Date.now();
            });
        }
        return {locations: c.locations};
    }
})();

getWeatherData();

// middleware to add weather data to context
app.use(function (req, res, next) {
    if (!res.locals.partials) res.locals.partials = {};
    res.locals.partials.weatherContext = getWeatherData();
    next();
});

app.use(function (req, res, next) {
    res.locals.flash = req.session.flash;
    delete req.session.flash;
    next();
});

var auth = require('./lib/auth.js')(app, {
    providers: credentials.authProviders,
    successRedirect: '/account',
    failureRedirect: '/unauthorized'
});
auth.init();
auth.registerRoutes();

app.get('/account', allow('customer,employee'), function (req, res) {

    res.render('account');
});

app.get('/sales', employeeOnly, function (req, res) {
    res.render('sales');
});

function customerOnly(req, res) {
    var user = req.session.passport.user;
    if (user && user.role === 'customer') return next();
    res.redirect(303, '/unauthorized');
}
function employeeOnly(req, res, next) {
    var user = req.session.passport.user;
    if (user && user.role === 'employee') return next();
    next('route');
}

function allow(roles) {
    return function (req, res) {
        var user = req.session.passport.user;
        if (user && roles.split(',').indexOf(user.role) !== -1) return next();
        res.redirect(303, '/unauthorized');
    }
}

require('./routes.js')(app);

app.get('/tours/hood-river', function (req, res) {
    res.render('tours/hood-river');
});
app.get('/tours/request-group-rate', function (req, res) {
    res.render('tours/request-group-rate');
});

app.get('/jquery-test', function (req, res) {
    res.render('jquery-test');
});

app.get('/nursery-rhyme', function (req, res) {
    res.render('nursery-rhyme');
});

app.get('/data/nursery-rhyme', function (req, res) {
    res.json({
        animal: 'squirrel',
        bodyPart: 'tail',
        adjective: 'bushy',
        noun: 'heck'
    });
});

app.get('/unauthorized', function (req, res) {
    res.render('unauthorized');
});

var VALID_EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;

app.post('/cart/checkout', function (req, res) {
    var cart = req.session.cart;
    if (!cart) next(new Error('Cart does not exist.'));
    var name = req.body.name || '', email = req.body.email || '';
    if (!email.match(VALID_EMAIL_REGEX)) {
        return res.next(new Error('Invalid email address.'));
    }
    cart.number = Math.random().toString().replace(/^0\.0*/, '');
    cart.billing = {
        name: name,
        email: email
    };
    res.render('email/cart-thank-you', {layout: null, cart: cart}, function (err, html) {
        if (err) console.log('error in email template');
        emailService.send(cart.billing.email, 'Your Meadowlark Travel Tour', html);
    });
    res.render('cart-thank-you', {cart: cart});
});

app.get('/set-currency/:currency', function (req, res) {
    req.session.currency = req.params.currency;
    return res.redirect(303, '/vacations');
});

var autoView = {};
app.use(function (req, res, next) {
    var path = req.path.toLowerCase();
    if (autoView[path]) return res.render(autoView[path]);
    if (fs.existsSync(__dirname + '/views' + path + '.hbs')) {
        autoView[path] = path.replace(/^\//, '');
        return res.render(autoView[path]);
    }
    next();
});

var Attraction = require('./models/attraction.js');
var rest = require('connect-rest');

var apiOptions = {
    context: '/api',
    domain: require('domain').create()
};
apiOptions.domain.on('error', function (err) {
    console.log('API domain error.\n', err.stack);
    setTimeout(function () {
        console.log('Server shutting down after API domain error.');
        process.exit(1);
    }, 5000);
    server.close();
    var worker = require('cluster').worker;
    if (worker) worker.disconnect();
});

app.use(rest.rester(apiOptions));
//app.use(vhost('api.*', rest.rester(apiOptions)));

rest.get('/attractions', function (req, content, cb) {
    Attraction.find({approved: true}, function (err, attractions) {
        if (err) return cb({error: 'Internal error.'});
        cb(null, attractions.map(function (a) {
            return {
                name: a.name,
                id: a._id,
                description: a.description,
                location: a.location
            }
        }));
    });
});

rest.post('/attraction', function (req, content, cb) {
    var a = new Attraction({
        name: req.body.name,
        description: req.body.description,
        location: {
            lat: req.body.lat,
            lng: req.body.lng
        },
        history: {
            event: 'created',
            email: req.body.email,
            date: new Date()
        },
        approved: false
    });
    a.save(function (err, a) {
        if (err) return cb({error: 'Unable to add attraction.'});
        cb(null, {id: a._id});
    })
});

rest.get('/attraction/:id', function (req, content, cb) {
    Attraction.findById(req.params.id, function (err, a) {
        if (err) return cb({error: 'Unable to retrieve attraction.'});
        cb(null, {
            name: a.name,
            id: a._id,
            description: a.description,
            location: a.location
        });
    });
});


app.use(function (req, res) {
    res.status(404);
    res.render('404');
});

app.use(function (err, req, res, next) {
    console.error(err.stack);
    res.status(500);
    res.render('500');
});

var server;
function startServer() {
    server = http.createServer(app).listen(app.get('port'), function () {
        console.log('Express started in ' + app.get('env') + ' mode on http://localhost:' + app.get('port') + '; press Ctrl-C to terminate.');
    });
}

if (require.main === module) {
    //应用程序直接运行；启动应用服务器
    startServer();
} else {
    //应用程序作为一个模块引入；导出函数
    module.exports = startServer;
}